package com.sudo.boot.service.sys.impl;

import cn.dev33.satoken.exception.NotPermissionException;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.boot.starter.redis.component.RedisKeys;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import com.sudo.boot.base.constant.SsoConstant;
import com.sudo.boot.base.constant.SysConstant;
import com.sudo.boot.base.constant.enums.EnumMenuType;
import com.sudo.boot.base.constant.enums.EnumStatus;
import com.sudo.boot.base.exception.BizException;
import com.sudo.boot.base.util.IfFail;
import com.sudo.boot.common.constant.ConstantSys;
import com.sudo.boot.mapper.sys.entity.SysMenu;
import com.sudo.boot.mapper.sys.entity.SysRole;
import com.sudo.boot.mapper.sys.entity.SysRoleMenu;
import com.sudo.boot.mapper.sys.mapper.SysMenuMapper;
import com.sudo.boot.mapper.sys.mapper.SysRoleMapper;
import com.sudo.boot.mapper.sys.mapper.SysRoleMenuMapper;
import com.sudo.boot.mapper.sys.model.bo.SysMenuBoAdd;
import com.sudo.boot.mapper.sys.model.bo.SysMenuBoAddSub;
import com.sudo.boot.mapper.sys.model.bo.SysMenuBoEdit;
import com.sudo.boot.mapper.sys.model.bo.SysMenuBoStatus;
import com.sudo.boot.mapper.sys.model.vo.SysMenuDetailVo;
import com.sudo.boot.mapper.sys.model.vo.SysMenuVo;
import com.sudo.boot.satokensso.util.SaRedisUtil;
import com.sudo.boot.service.sys.SysMenuService;
import com.sudo.boot.service.sys.converter.SysMenuConverter;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.*;

import static com.sudo.boot.mapper.sys.entity.table.SysMenuTableDef.SYS_MENU;

/**
 * 系统菜单  服务层实现。
 *
 * @author admins
 * @since 2023-10-13
 */
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
    @Resource
    private SaRedisUtil saRedisUtil;
    @Resource
    private SysMenuConverter sysMenuConverter;
    @Resource
    private SysRoleMenuMapper sysRoleMenuMapper;
    @Resource
    private SysRoleMapper sysRoleMapper;
    @Resource
    private SysRedisService sysRedisService;

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    @Override
    public void add(SysMenuBoAdd bo) {

        // 1.如果添加目录
        if (EnumMenuType.D.getCode().equals(bo.getMenuType())) {
            // 将无用字段置空
            bo.setComponentUrl(null);
            // 将目录路由设置为menuCode，避免前端菜单异常
            bo.setRouteUrl(bo.getMenuCode());
        }

        // 2.如果添加菜单
        if (EnumMenuType.M.getCode().equals(bo.getMenuType())) {
            Assert.notNull(bo.getRouteUrl(),"路由不能为空");
        }

        // 3.如果添加按钮
        if (EnumMenuType.B.getCode().equals(bo.getMenuType())) {
            throw new BizException("不能添加按钮");
        }

        // 4.判断菜单代码是否占用
        SysMenu existMenu = mapper.selectOneByCondition(SYS_MENU.MENU_CODE.eq(bo.getMenuCode()));
        Assert.isNull(existMenu,"菜单代码已被占用");

        // 5.新增菜单
        SysMenu sysMenu = sysMenuConverter.toSysmenu(bo);
        sysMenu.setStatus(EnumStatus.ON.getCode());
        sysMenu.setParentCode(SysConstant.root_menu);
        IfFail.dealInt(() -> mapper.insert(sysMenu));

        // 6.给超级管理员添加权限
        SysRoleMenu sysRoleMenu = new SysRoleMenu();
        sysRoleMenu.setMenuCode(bo.getMenuCode());
        sysRoleMenu.setRoleCode(SysConstant.super_role);
        IfFail.dealInt(() -> sysRoleMenuMapper.insert(sysRoleMenu));

        // 清空超级管理员菜单或按钮缓存
        sysRedisService.removeMenuOrButtonOfRole(sysMenu,SysConstant.super_role);
    }

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    @Override
    public void addSub(SysMenuBoAddSub bo) {
        // 1.查询父菜单
        SysMenu parentSysMenu = mapper.selectOneByCondition(SYS_MENU.MENU_CODE.eq(bo.getParentCode()));
        Assert.notNull(parentSysMenu,"找不到父菜单");

        // 2.判断菜单代码是否占用
        SysMenu existMenu = mapper.selectOneByCondition(SYS_MENU.MENU_CODE.eq(bo.getMenuCode()));
        Assert.isNull(existMenu,"菜单代码已被占用");

        // 4.如果添加目录
        if (EnumMenuType.D.getCode().equals(bo.getMenuType())) {
            if (!EnumMenuType.D.getCode().equals(parentSysMenu.getMenuType())) {
                throw new BizException("目录只能挂在目录上");
            }
            // 将无用字段置空
            bo.setComponentUrl(null);
            // 将目录路由设置为menuCode，避免前端菜单异常
            bo.setRouteUrl(bo.getMenuCode());
        }

        // 5.如果添加菜单
        if (EnumMenuType.M.getCode().equals(bo.getMenuType())) {
            if (!EnumMenuType.D.getCode().equals(parentSysMenu.getMenuType())) {
                throw new BizException("菜单只能挂在目录上");
            }

            Assert.notNull(bo.getRouteUrl(),"路由不能为空");
        }

        // 4.如果添加按钮
        if (EnumMenuType.B.getCode().equals(bo.getMenuType())) {
            if (!EnumMenuType.M.getCode().equals(parentSysMenu.getMenuType())) {
                throw new BizException("按钮只能挂在菜单上");
            }

            // 将无用字段置空
            bo.setComponentUrl(null);
            bo.setRouteUrl(null);
        }
        // 7.新增菜单
        SysMenu sysMenu = sysMenuConverter.toSysmenu(bo);
        sysMenu.setStatus(EnumStatus.ON.getCode());
        IfFail.dealInt(() -> mapper.insert(sysMenu));

        // 8.给超级管理员添加权限
        SysRoleMenu sysRoleMenu = new SysRoleMenu();
        sysRoleMenu.setMenuCode(bo.getMenuCode());
        sysRoleMenu.setRoleCode(SysConstant.super_role);
        IfFail.dealInt(() -> sysRoleMenuMapper.insert(sysRoleMenu));

        // 清空超级管理员菜单或按钮缓存
        sysRedisService.removeMenuOrButtonOfRole(sysMenu,SysConstant.super_role);
    }

    @Override
    public void edit(SysMenuBoEdit bo) {
        // 1.查询当前菜单
        SysMenu oldSysMenu = mapper.selectOneById(bo.getId());

        SysMenu newSysMenu = sysMenuConverter.toSysMenu(bo);

        // 2.如果目录
        if (!EnumMenuType.M.getCode().equals(oldSysMenu.getMenuType())) {
            // 置空则不会被修改
            newSysMenu.setRouteUrl(null);
            newSysMenu.setComponentUrl(null);
        }

        IfFail.dealInt(() -> mapper.update(newSysMenu));
    }

    @Override
    public void status(SysMenuBoStatus bo) {
        // 1.查询菜单
        SysMenu sysMenu = mapper.selectOneById(bo.getId());

        // 2.切换状态
        SysMenu statusMenu = new SysMenu();
        statusMenu.setId(bo.getId());
        statusMenu.setStatus(bo.getStatus());
        IfFail.dealInt(() -> mapper.update(statusMenu));

        // 3.菜单缓存
        sysRedisService.removeMenuOrButtonOfAllRole(sysMenu);
    }

    @Override
    public SysMenuDetailVo detail(Long id) {
        // 1.查询当前菜单
        SysMenu sysMenu = mapper.selectOneById(id);
        Assert.notNull(sysMenu, "当前菜单不存在");

        SysMenuDetailVo detailVo = sysMenuConverter.toSysMenuDetailVo(sysMenu);
        // 2.判断父菜单是否为0
        if (SysConstant.root_menu.equals(sysMenu.getParentCode())) {
            detailVo.setParentName("根目录");
            return detailVo;
        }

        // 3.查询父菜单
        SysMenu parentMenu = mapper.selectOneByCondition(SYS_MENU.MENU_CODE.eq(sysMenu.getParentCode()));
        detailVo.setParentName(parentMenu.getMenuName());
        return detailVo;
    }

    @Override
    public List<SysMenuVo> selectAllSysMenuList(List<String> menuTypeList) {
        List<SysMenu> sysMenuList;
        if (CollectionUtils.isEmpty(menuTypeList)) {
            sysMenuList = mapper.selectAll();
        }else {
            sysMenuList = mapper.selectListByCondition(SYS_MENU.MENU_TYPE.in(menuTypeList));
        }
        return sysMenuConverter.toSysMenuVoList(sysMenuList);
    }

    @Override
    public List<Tree<String>> selectAllSysMenuTree() {
        return buildNodeList(selectAllSysMenuList(null));
    }

    private List<Tree<String>> buildNodeList(List<SysMenuVo> sysMenuVoList) {
        TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
        treeNodeConfig.setIdKey(CharSequenceUtil.toCamelCase(SYS_MENU.MENU_CODE.getName()));
        treeNodeConfig.setParentIdKey(CharSequenceUtil.toCamelCase(SYS_MENU.PARENT_CODE.getName()));
        treeNodeConfig.setChildrenKey(ConstantSys.MENU_CHILDREN);
        treeNodeConfig.setWeightKey(CharSequenceUtil.toCamelCase(SYS_MENU.SORT.getName()));
        treeNodeConfig.setNameKey(CharSequenceUtil.toCamelCase(SYS_MENU.MENU_NAME.getName()));
        treeNodeConfig.setDeep(5);

        return TreeUtil.build(sysMenuVoList, SysConstant.root_menu, treeNodeConfig, (treeNode, tree) -> {
            tree.setId(treeNode.getMenuCode());
            tree.setParentId(treeNode.getParentCode());
            tree.setName(treeNode.getMenuName());
            tree.setWeight(treeNode.getSort());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.ID.getName()), treeNode.getId());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.MENU_ICON.getName()), treeNode.getMenuIcon());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.MENU_TYPE.getName()), treeNode.getMenuType());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.ROUTE_URL.getName()), treeNode.getRouteUrl());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.STATUS.getName()), treeNode.getStatus());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.NOTE.getName()), treeNode.getNote());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.COMPONENT_URL.getName()), treeNode.getComponentUrl());
        });
    }

    @Override
    public List<Tree<String>> selectAllForLayMenuTree() {
        List<SysMenuVo> menuVoList = selectAllSysMenuList(null);

        TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
        treeNodeConfig.setIdKey(CharSequenceUtil.toCamelCase(SYS_MENU.MENU_CODE.getName()));
        treeNodeConfig.setParentIdKey(CharSequenceUtil.toCamelCase(SYS_MENU.PARENT_CODE.getName()));
        treeNodeConfig.setChildrenKey(ConstantSys.MENU_CHILDREN);
        treeNodeConfig.setWeightKey(CharSequenceUtil.toCamelCase(SYS_MENU.SORT.getName()));
        treeNodeConfig.setNameKey("title");
        treeNodeConfig.setDeep(5);

        return TreeUtil.build(menuVoList, SysConstant.root_menu, treeNodeConfig, (treeNode, tree) -> {
            tree.setId(treeNode.getMenuCode());
            tree.setParentId(treeNode.getParentCode());
            tree.setName(treeNode.getMenuName());
            tree.setWeight(treeNode.getSort());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.ID.getName()), treeNode.getId());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.MENU_ICON.getName()), treeNode.getMenuIcon());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.MENU_TYPE.getName()), treeNode.getMenuType());
            tree.putExtra(CharSequenceUtil.toCamelCase(SYS_MENU.STATUS.getName()), treeNode.getStatus());
        });
    }

    /**
     * 根据角色获取菜单
     * @param roleCode 角色代码
     * @param menuTypeList 菜单类型
     */
    @Override
    public List<SysMenuVo> selectMenuListByRoleCode(String roleCode, List<String> menuTypeList) {
        return mapper.selectMenuListByRoleCode(roleCode,menuTypeList);
    }

    /**
     * 根据角色获取菜单树
     * @param roleCode 角色代码
     */
    @Override
    public List<Tree<String>> selectMenuTreeByRoleCode(String roleCode) {
        List<String> menuTypeList = List.of(EnumMenuType.D.getCode(), EnumMenuType.M.getCode());
        List<SysMenuVo> menuVoList = selectMenuListByRoleCode(roleCode, menuTypeList);
        menuVoList = menuVoList.stream().filter(o -> EnumStatus.ON.getCode().equals(o.getStatus())).toList();
        return buildNodeList(menuVoList);
    }

    /**
     * 根据角色获取菜单树
     * @param roleCode 角色代码
     */
    @Override
    public List<Tree<String>> selectSimulateMenuTreeByRoleCode(String roleCode) {
        List<String> menuTypeList = List.of(EnumMenuType.D.getCode(), EnumMenuType.M.getCode());
        List<SysMenuVo> menuVoList = selectMenuListByRoleCode(roleCode, menuTypeList);
        menuVoList = menuVoList.stream()
                .filter(o -> EnumStatus.ON.getCode().equals(o.getStatus()))
                .peek(o-> o.setRouteUrl(SsoConstant.simulatePath + o.getRouteUrl()))
                .toList();
        return buildNodeList(menuVoList);
    }

    /**
     * 根据角色获取菜单代码
     */
    @Override
    public List<Integer> selectMenuIdListByRoleId(Integer roleId) {
        // 获取角色
        SysRole sysRole = sysRoleMapper.selectOneById(roleId);

        List<SysMenuVo> menuVoList = selectMenuListByRoleCode(sysRole.getRoleCode(), null);
        if (CollectionUtils.isEmpty(menuVoList)) {
            return Collections.emptyList();
        }
        return menuVoList.stream().map(SysMenuVo::getId).toList();
    }

    /**
     * 根据角色、菜单，获取角色对于菜单的按钮权限
     * @param currRoleCode 角色
     * @param menuCode 菜单
     */
    @Override
    public void checkMenuAuth(String currRoleCode, String menuCode) {
        // 判断角色菜单缓存是否存在
        String roleMenuKey = RedisKeys.roleMenu(currRoleCode);
        boolean hasKey = saRedisUtil.hasKey(roleMenuKey);

        if (hasKey){
            String menuStatus = String.valueOf(saRedisUtil.hget(roleMenuKey, menuCode));
            if (StringUtils.hasText(menuStatus)) {
                if (!EnumStatus.ON.getCode().equals(menuStatus)) {
                    throw new NotPermissionException("该菜单已禁用");
                }
            }else {
                throw new NotPermissionException("暂无该菜单权限");
            }
        }else {
            // 加载菜单到缓存
            List<SysMenuVo> menuVoList = selectMenuListByRoleCode(currRoleCode, List.of(EnumMenuType.M.getCode()));
            Map<String, Object> menuMap = new HashMap<>();
            if (CollectionUtils.isEmpty(menuVoList)) {
                menuMap.put(SysConstant.root_menu, EnumStatus.OFF.getCode());
            }else {
                for (SysMenuVo sysMenuVo : menuVoList) {
                    menuMap.put(sysMenuVo.getMenuCode(), sysMenuVo.getStatus());
                }
            }
            saRedisUtil.hPutAll(roleMenuKey, menuMap);
            // 重新检查
            checkMenuAuth(currRoleCode, menuCode);
        }
    }

    /**
     * 根据菜单获取按钮
     * @param menuCode 菜单
     */
    @Override
    public List<SysMenuVo> selectMenuListByRoleCode(String menuCode) {
        QueryWrapper queryWrapper = QueryWrapper.create()
                .where(SYS_MENU.PARENT_CODE.eq(menuCode))
                .and(SYS_MENU.MENU_TYPE.eq(EnumMenuType.B.getCode()));
        return mapper.selectListByQueryAs(queryWrapper, SysMenuVo.class);
    }

    @Override
    public void deleteById(Integer id) {
        SysMenu sysMenu = mapper.selectOneById(id);

        // 判断当前菜单是否存在子级，如果存在则不能删除
        QueryWrapper queryWrapper = QueryWrapper.create()
                .where(SYS_MENU.PARENT_CODE.eq(sysMenu.getMenuCode()));
        long count = mapper.selectCountByQuery(queryWrapper);
        Assert.isTrue(count <= 0, "存在子级，不能删除");

        // 查询改菜单
        SysMenu deleteSysMenu = mapper.selectOneById(id);

        // 删除菜单
        IfFail.dealInt(() -> mapper.deleteById(id));

        // 清除缓存
        sysRedisService.removeMenuOrButtonOfAllRole(deleteSysMenu);
    }

    @Override
    public List<String> getRoleButtonList(String roleCode) {
        // 判断角色菜单缓存是否存在
        String roleButtonKey = RedisKeys.roleButton(roleCode);
        boolean hasKey = saRedisUtil.hasKey(roleButtonKey);

        if (hasKey){
            Map<Object, Object> roleButtonMap = saRedisUtil.hEntries(roleButtonKey);
            Set<Map.Entry<Object, Object>> entries = roleButtonMap.entrySet();

            List<String> buttonList = new ArrayList<>();
            for (Map.Entry<Object, Object> entry : entries) {
                String buttonCode = String.valueOf(entry.getKey());
                String buttonStatus = String.valueOf(entry.getValue());
                if (EnumStatus.ON.getCode().equals(buttonStatus)) {
                    buttonList.add(buttonCode);
                }
            }
            return buttonList;
        }else {
            // 加载按钮到缓存
            List<SysMenuVo> buttonVoList = mapper.selectMenuListByRoleCode(roleCode, List.of(EnumMenuType.B.getCode()));
            Map<String, Object> buttonMap = new HashMap<>();
            if (CollectionUtils.isEmpty(buttonVoList)) {
                buttonMap.put(SysConstant.root_menu, EnumStatus.OFF.getCode());
            }else {
                for (SysMenuVo sysMenuVo : buttonVoList) {
                    buttonMap.put(sysMenuVo.getMenuCode(), sysMenuVo.getStatus());
                }
            }
            saRedisUtil.hPutAll(roleButtonKey, buttonMap);
            // 重新检查
            return getRoleButtonList(roleCode);
        }
    }
}
